React Router を v5 から v6 にアップデートしてみました
MAD 事業部の高橋です。
少し前に React Router を v5 から v6 にアップデートしたので、行った作業を振り返ってまとめてみました。一気に v6 の機能をフルで使うというよりも、最小限の対応で v6 にアップデートするイメージです。
公式サイトのアップデート手順をまずはご参考にしていただき、実際にどんな感じだったのかの雰囲気を感じていただければなと思います。
行った作業は以下となりました。
useParams
の型の変更に対応<NavLink />
の型の変更に対応<Switch />
を<Routes />
に変更<Route />
の型の変更に対応<Redirect />
を<Navigate />
に変更useHistory
をuseNavigate
に変更useRouteMatch
をuseMatch
に変更
解説していきます。
useParams
の型の変更に対応
v5 自体には例えば /user/:userId/content/:contentId
のようなパスのパスパラメータを取得する場合には次の用に記述していました。
type ParhParams = { userId: string; contentId: string; }; const { userId, contentId } = useParams<Params>();
v6 からの型は次のように定義されるようになりました。
export declare function useParams<Key extends string = string>(): Readonly< Params<Key> >; export declare type Params<Key extends string = string> = { readonly [key in Key]: string | undefined; }; // つまり以下のように書くと // userId と contentId の型は `string | undefined` となる const { userId, contentId } = useParams<"userId" | "contentId">();
そのため、undefined
だった場合の対応が必要になりました。userId ?? ""
のようにしてしまってもいいのですが、Assertion Functions を使って対応しました。
function assertIsDefined<T>(val: T): asserts val is NonNullable<T> { if (val === undefined || val === null) { throw new AssertionError( `Expected 'val' to be defined, but received ${val}` ); } }
<NavLink />
の型の変更に対応
props の exact
を end
に単純なリネームが必要になります。
場合によっては大きく修正が必要になりそうなものが activeClassName
と activeStyle
の廃止です。置き換えの対象プロジェクトが Material UI v5 を使用しており、次のように記述できませんでした。
import { Link } from "@mui/material"; import { NavLink } from "react-router-dom"; <Link className={({ isActive }) => (isActive ? "isActive" : "")} component={NavLink} to="/" > {children} </Link>;
問題になっていた箇所が数えるほどだったため、次のように対応しました。
<Box sx={{ "> .isActive": { color: "white" } }}> <NavLink className={({ isActive }) => (isActive ? "isActive" : "")} {...rest}> {children} </NavLink> </Box>
公式で解説されているようにラッパーを用意するのも良さそうです。
<Switch />
を <Routes />
に変更
アップデートの対象プロジェクトが Switch をネストしていなかったため、単純な置き換えのみでした。
<Route />
の型の変更に対応
exact
が不要になったため削除しました。
component
を element
に変更するのは単純な作業でしたが、認証に Auth0 を使用していたため、次のようなコンポーネントを作成していました。
const ProtectedRoute = (props: RouteProps) => { const { component, ...rest } = props; return ( <Route component={withAuthenticationRequired(component as React.ComponentType)} {...rest} /> ); }; // <ProtectedRoute exact path="/" component={HomePage} /> // 上記のように使う
element={withAuthenticationRequired(component)}
とはできないため、次のように対応しました。
const Protected = withAuthenticationRequired(Outlet); // <Route element={<Protected />}> // <Route path="/" element={<HomePage />} /> // </Route> // 上記のように使う
<Redirect />
を <Navigate />
に変更
v5 では push の場合には props で指定が必要ですが、v6 ではデフォルトが push になり、replace の場合に replace の指定が必要となります。
// v5 <Redirect to="/foo" /> <Redirect to="/bar" push /> // v6 <Navigate to="/foo" replace /> <Navigate to="/bar" />
useHistory
を useNavigate
に変更
// v5 const history = useHistory(); history.push("/"); history.replace("/"); history.goBack(); // v6 const navigate = useNavigate(); navigate("/"); navigate("/", { replace: true }); navigate(-1);
<Navigate />
と同様にデフォルトが push、replace の場合には指定が必要になります。go
や goBack
、goForward
の場合には引数に number を与えます。
useRouteMatch
を useMatch
に変更
v5 から v6 になったことで、コンポーネント側から現在の URL の Path Pattern (/user/:userId
のようなやつ)を取得する方法が変わりました。
v5 では単純に useRouteMatch
を使うだけで良かったのですが、useMatch
は Path Pattern を取得するために 確認したい Path Pattern を引数にわたす必要があります。
// v5 const match = useRouteMatch(); // v6 const match = useMatch("/user/:userId");
このため、条件に複数の Path Pattern がある場合には useMatch
を使用できません。
例えば、/foo
あるいは /bar
のどちらかの URL だった場合に何かをしたいような場合、useMatch
ではなく matchPath
を使う必要があります。
const { pathname } = useLocation(); const match = useMemo(() => { return ["/foo", "/bar"].find((path) => !!matchPath(path, pathname)); }, [pathname]);
まとめ
useRoutes
や <Route />
のネスト、Layout Routes などを使ってより良いコードにすることも、場合によってはできそうです。
ただしチーム開発のパッケージのアップデートの PR には、必要最小限のコードの修正のみに絞っていたほうがレビュワーに優しいのではないかと考えており、この記事もそこに焦点を書いてみました。